/*
 *  Copright: 2008 Philipp Leitner, Distributed Systems Group,
 *            TU Wien, leitner@infosys.tuwien.ac.at
 *
 *  This file is part of DAIOS.
 *
 *  DAIOS is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as
 *  published by the Free Software Foundation.
 * 
 *  DAIOS is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with DAIOS.  If not, see
 *  <http://www.gnu.org/licenses/>.
 * 
 */

package at.ac.tuwien.infosys.dsg.daiosPlugins.nativeInvoker.backend;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.URL;
import java.util.Hashtable;
import java.util.logging.Logger;

import javax.xml.stream.XMLStreamException;

import org.apache.axiom.om.OMElement;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.commons.httpclient.params.DefaultHttpParams;
import org.apache.commons.httpclient.params.HttpClientParams;

import at.ac.tuwien.infosys.dsg.daios.exceptions.HTTPException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.InvocationException;
import at.ac.tuwien.infosys.dsg.daios.framework.interfaces.IConstants;

/**
 * A backend for firing HTTP invocations based on Apache HTTPClient.
 * 
 * @author philipp
 *
 */
public class HTTPBackend {
	
	private URL endpoint = null;
	private Logger log = null;
	private HttpClient client = null;
	private HttpMethod method = null;
	private Hashtable<String, String> httpHeaders = null;
	private Hashtable<String, String> responseHeaders = null;
	private boolean soap = false;

	/**
	 * Construct a new HTTP backend to a given URL identifier. Do
	 * not set any specific HTTP header fields.
	 * 
	 * @param endpoint The URL to connect to with this HTTP backend.
	 * @param log The logger to use
	 */
	public HTTPBackend(URL endpoint, Logger log) {
		this.endpoint = endpoint;
		this.log = log;
		this.soap = false;
	}	
	
	/**
	 * Construct a new HTTP backend to a given URL identifier. Also set
	 * a list of specific HTTP headers.
	 * 
	 * @param endpoint The URL to connect to with this HTTP backend.
	 * @param log The logger to use
	 * @param httpHeaders A list of additional HTTP headers as String/Value
	 * list
	 * @param soapheader Should we set a 'soap-action' header as required by
	 * Axis2?
	 */
	public HTTPBackend(URL endpoint, Logger log,
			Hashtable<String, String> httpHeaders, boolean soapheader) {
		this.endpoint = endpoint;
		this.log = log;
		this.soap = soapheader;
		this.httpHeaders = httpHeaders;
	}
	
	/**
	 * Do a HTTP GET invocation to the endoint associated with this backend.
	 * 
	 * @return The body of the HTTP response. Note that the HTTP response
	 * headers can be accessed by invoking getHTTPResponseHeaders().
	 * 
	 * @throws InvocationException
	 */
	public InputStream httpGetInvocation() throws InvocationException {
		
		log.info("Doing a HTTP GET invocation to endpoint "+
			endpoint.toExternalForm());
		
		try {
			// construct a new HTTPClient client
			newClient();
			// construct a new HTTPClient method
			newMethod(endpoint, IConstants.HTTP_GET_METHOD);
			// execute the method
			client.executeMethod(method);
			if(success()) {
				log.info("Successfully issued HTTP GET invocation.");
				InputStream is = method.getResponseBodyAsStream();
				// save the HTTP response headers
				saveHeaders();
//				method.releaseConnection();
				return is;
			}
			else 
				throw new HTTPException(method.getStatusCode());
		} catch(Exception e) {
			e.printStackTrace();
//			method.releaseConnection();
			log.severe("Caught exception while invoking service:");
			throw new InvocationException(e);
		}
	    
		
	}
	
	
	/**
	 * Do a HTTP POST invocation to the endoint associated with this backend,
	 * and add a given String as HTTP request body. The body will be
	 * transmitted 'chunked'.
	 * 
	 * @param body The String that should be added as request body. 
	 * @return The body of the HTTP response. Note that the HTTP response
	 * headers can be accessed by invoking getHTTPResponseHeaders().
	 * 
	 * @throws InvocationException
	 */
	public InputStream httpPostInvocation(OMElement body) throws InvocationException {
		
		log.info("Doing a HTTP POST invocation to endpoint "+
			endpoint.toExternalForm());
		
		try {
			// construct a new HTTPClient client			
			newClient();
			// construct a new HTTPClient POST method			
			newMethod(endpoint, IConstants.HTTP_POST_METHOD);
			// set the body of the request as streamed entity
			RequestEntity bodyEntity = new
				DaiosRequestEntity(body);
			((PostMethod)method).setRequestEntity(bodyEntity);
			// execute
			client.executeMethod(method);
			if(success()) {
				log.info("Successfully issued HTTP POST invocation.");
				InputStream resp = method.getResponseBodyAsStream();
				// save headers for later retrival
				saveHeaders();
//				method.releaseConnection();
				return resp;
			}
			else 
				throw new HTTPException(method.getStatusCode());
		} catch(Exception e) {
			e.printStackTrace();
//			method.releaseConnection();
			log.severe("Caught exception while invoking service:");
			throw new InvocationException(e);
		}
		
	}
	
	/**
	 * Do a HTTP GET invocation to the endoint associated with this backend,
	 * but ignore the response and do not report any errors.
	 * 
	 * @throws InvocationException
	 */	
	public void httpGetInvocationOneway() throws InvocationException {
		
		log.info("Doing a HTTP GET oneway invocation to endpoint "+
			endpoint.toExternalForm());
		
		try {
			newClient();
			newMethod(endpoint, IConstants.HTTP_GET_METHOD);
			client.executeMethod(method);
			method.releaseConnection();
		} catch(Exception e) {
			log.severe("Caught exception while invoking service:");
			e.printStackTrace();
			// do not give an error for fire and forget
		}
		
	}
	
	/**
	 * Do a HTTP POST invocation to the endoint associated with this backend,
	 * and add a given String as HTTP request body. The body will be
	 * transmitted 'chunked'. Do not return the result and do not report
	 * any errors.
	 * 
	 * @param body The String that should be added as request body. 
	 * 
	 * @throws InvocationException
	 */
	public void httpPostInvocationOneway(OMElement body) throws InvocationException {
		
		log.info("Doing a HTTP POST invocation to endpoint "+
			endpoint.toExternalForm());
		
		try {
			newClient();
			newMethod(endpoint, IConstants.HTTP_POST_METHOD);
			RequestEntity bodyEntity = new
				DaiosRequestEntity(body);
			((PostMethod)method).setRequestEntity(bodyEntity);
			client.executeMethod(method);
//			method.releaseConnection();
		} catch(Exception e) {
			log.severe("Caught exception while invoking service:");
			e.printStackTrace();
			// do not give an error for fire and forget
		}
		
	}	
	
	/**
	 * Access the HTTP response headers of the last invocation.
	 * 
	 * @return The HTTP response headers of the last invocation as
	 * Hashtable of Strings. Null if the invocation has not been
	 * fired yet.
	 */
	public Hashtable<String, String> getHTTPResponseHeaders() {
		return responseHeaders;
	}
	
	/**
	 * Create a new HTTPClient client. This sets up the HTTPBackend
	 * for invocation. 
	 *
	 */
	private void newClient() {
		
		// set parameter set to default params
		HttpClientParams params = new HttpClientParams(
			 DefaultHttpParams.getDefaultParams());
		// make the invocation 'lenient', i.e. not strict
		params.makeLenient();
		
		// create a new client
		client = new HttpClient(params);
		
	}
	
	/**
	 * Create a new HTTP method of a given type to a given
	 * endpoint.
	 * 
	 * @param endpoint The endpoint to connect to.
	 * @param type  The type of the method to construct. Supported
	 * types are 'GET' and 'POST'.
	 */
	private void newMethod(URL endpoint, String type) {
		
		// reset saved response headers
		responseHeaders = null;
		
		if(type.equals(IConstants.HTTP_GET_METHOD)) {
			method = new GetMethod(endpoint.toExternalForm());
			addGETHeaders(method);
		}
		else {
			method = new PostMethod(endpoint.toExternalForm());
			addPOSTHeaders(method);
		}
	}
	
	/**
	 * Add default and user-specific HTTP GET headers to a method.
	 * 
	 * @param method
	 */
	private void addGETHeaders(HttpMethod method) {
		
		addUserDefinedHeaders(method);
		
		if(method.getRequestHeader("Connection") == null)
			method.addRequestHeader("Connection", "Keep-Alive");
		if(method.getRequestHeader("Content-Type") == null)
			method.addRequestHeader("Content-Type", "text/xml; charset=UTF-8");
		if(soap && method.getRequestHeader("SOAPAction") == null)
			method.addRequestHeader("SOAPAction", "urn:anonOutInOp");
		
	}

	/**
	 * Add default and user-specific HTTP POST headers to a method.
	 * 
	 * @param method
	 */	
	private void addPOSTHeaders(HttpMethod method) {
		
		addUserDefinedHeaders(method);
		
		((PostMethod)method).setContentChunked(true);
		
		if(method.getRequestHeader("Connection") == null)
			method.addRequestHeader("Connection", "Keep-Alive");
		if(method.getRequestHeader("Content-Type") == null)
			method.addRequestHeader("Content-Type", "text/xml; charset=UTF-8");
		if(soap && method.getRequestHeader("SOAPAction") == null)
			method.addRequestHeader("SOAPAction", "urn:anonOutInOp");
		
	}
	
	/**
	 * Add user-specific HTTP headers to a method.
	 * 
	 * @param method
	 */	
	private void addUserDefinedHeaders(HttpMethod method) {
		
		if(httpHeaders == null)
			return;
		
		for(String header : httpHeaders.keySet()) {
			method.addRequestHeader(header, httpHeaders.get(header));
		}
		
	}
	
	/**
	 * Check if a method ended 'successfully'. In that context
	 * we assume that every invocation that found a listening
	 * service is some kind of success (even if the actual
	 * invocation threw an error).
	 * 
	 * @return True if the method ended successfully (after above
	 * definition), false otherwise.
	 */
	private boolean success() {
		// we assume that everything except 4** errorcodes are
		// some kind of "success" ... even an error message
		// should be processed
		return ((method.getStatusCode()/100) != 4);
	}
	
	/**
	 * Save HTTP response headers for later retrieval.
	 *
	 */
	private void saveHeaders() {
		responseHeaders = new Hashtable<String, String>();
		Header[] headers = method.getResponseHeaders();
		for(Header h : headers)
			responseHeaders.put(h.getName(), h.getValue());
	}
	
	class DaiosRequestEntity implements RequestEntity {
		
	    private OMElement body = null;

	    public DaiosRequestEntity(OMElement body) {
	    	this.body = body;
	    }

	    public boolean isRepeatable() {
	        return true;
	    }

	    public void writeRequest(OutputStream outStream) throws IOException {
	    	try {
	    		// serialize the OMElement to the given output stream
				body.serialize(outStream);
			} catch (XMLStreamException e) {
				e.printStackTrace();
				throw new IOException(e.getMessage());
			}
	    }

	    public long getContentLength() {
	        // we stream our content, we therefore have no clue how long
	    	// its gonna be
	    	return -1;
	    }

	    public String getContentType() {
	    	// we should always transport XML content
	    	return "text/xml";
	    }
		
	}
	
}
